Repository navigation
Recover from non-monotonic presentation timestamps in MP4 video tracks without ctts - #3352
2bitoperations wants to merge 3 commits into
Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
|
Pushed a fix for a second bug: audio/video would slowly drift out of Tested in two real players on the affected hardware (Jellyfin Android |
|
Thanks for the detailed investigation and for providing a legal, However, I don't think the current evidence isolates this as a I downloaded After decoding, the propagated frame PTS becomes non-monotonic: while FFmpeg's This looks consistent with missing or incorrect composition offsets Android documents https://developer.android.com/reference/android/media/MediaCodec.BufferInfo#presentationTimeUs Therefore, reproducing the same output timestamp sequence with Could we rule this out before changing
There is also a separate scope concern with the current patch: despite The patch explains why this particular CFR sample becomes smooth, but |
SummaryMANY thanks for your helpful reply. We looked into all three of your suggestions. Short version: I no longer think this is a 1. Composition timestamps: correctly-muxed vs. naively-muxed contentYou were right about the repro file. Reproduced your The repro README's mux step ( ffmpeg -f lavfi -i "testsrc2=size=640x360:rate=24:duration=15" -f yuv4mpegpipe src.y4m
# encode straight to MP4 — libavformat assigns real composition offsets in-process
ffmpeg -i src.y4m -c:v libx265 -tag:v hvc1 good.mp4
# encode to a detached raw stream, then remux exactly like the repro README does
ffmpeg -i src.y4m -c:v libx265 -f hevc raw.hevc
ffmpeg -fflags +genpts -r 24 -i raw.hevc -c:v copy -tag:v hvc1 bad.mp4Same encoder, same options, same source — only the mux step differs. Confirmed on-device too, on a build with none of our patches applied: correctly-muxed B-frame 2. x265 3.3 vs. current x265 4.1Re-encoded the same Tears of Steel source with current x265 (4.1+1-1d117be), options matched as This briefly looked like it contradicted our own earlier finding that x265 4.1 content didn't So: not a contradiction, and not an x265-version-specific bug. Encoder version is irrelevant to 3. Logging queued input vs. dequeued output timestampsPatch (diagnostic-only, no behavior change):
Queued-input order is perfectly monotonic in both — a direct echo of the container's decode-order The key check: every single dequeued output timestamp, in both files, is one of the exact Where this leaves thingsI don't think this is evidence of a MediaTek decoder defect. However, what it does look like is VLC plays these incorrectly-timestamped files just fine, so I want to dig into why. My guess is |
Why VLC doesn't stutter on this contentInstrumented VLC itself (not just read its source) and ran it on-device against the same repro file. It receives equally or more disordered decoder output than ExoPlayer from VLC just never lets that value reach anything that schedules a frame. Its MP4 demuxer refuses to invent a PTS for a video track when the file has no // mp4.c, https://code.videolan.org/videolan/vlc/-/blob/ac6c2a405d652b5576128ceb9fec2c342f0e83ec/modules/demux/mp4/mp4.c#L1391-L1396
if( MP4_TrackGetPTSDelta( p_demux, tk, &i_delta ) )
p_block->i_pts = p_block->i_dts + i_delta;
else if( tk->fmt.i_cat != VIDEO_ES )
p_block->i_pts = p_block->i_dts;
else
p_block->i_pts = VLC_TICK_INVALID; // video track, no ctts: don't guessThat
// BoxParser.java, https://github.com/androidx/media/blob/5fb306449733dd71595700c1227ad6087578c559/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/BoxParser.java#L516
int timestampOffset = 0;
...
// https://github.com/androidx/media/blob/5fb306449733dd71595700c1227ad6087578c559/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/BoxParser.java#L627
timestamps[i] = timestampTimeUnits + timestampOffset; // ctts == null → always 0 → PTS = DTSThat definite-but-wrong PTS is exactly what So: not a MediaTek defect, not an ExoPlayer bug in the usual sense — one demuxer treats a missing Next: I'm going to look at overhauling this to give media3 a first-class notion of "missing PTS," and only fall back to today's spray-and-pray frame-dropping behavior in that specific case. |
|
ok - i think this approach is way more contained and correct. @FongMi please let me know what you think! |
|
This is much better scoped than the previous version, but I found several correctness issues that should be addressed before this is ready:
Please also add focused tests covering:
The investigation and the move away from a synthetic CFR timeline are solid improvements, but the current implementation still has correctness and lifecycle gaps. |
Targets a specific edge case in the fix proposed in androidx/media#3352: missing ctts + a codec (MPEG-4 Part 2) media3 never derives maxNumReorderSamples for, so the conservative NO_VALUE handling is what has to catch it rather than positive evidence of reordering.
|
Addressed point 2 in |
…hout ctts Squash of androidx#3352 (325f2c5..410db95, previously based on release) applied to main. Conflicts were resolved mechanically: main has since removed Format#hasPrerollSamples, and its MediaCodecRenderer additions sit next to the ones in this change. MP4 video tracks that reorder frames but have no ctts box give every sample a presentation time equal to its decode time, for example a raw H.265 elementary stream muxed with ffmpeg -fflags +genpts -c:v copy. MediaCodec echoes those timestamps back on frames that come out in display order, so the renderer sees non-monotonic timestamps and drops ~30% of frames as late. See androidx#3347. - Format#hasReliablePresentationTimestamps, set false by BoxParser for a video track with no ctts and maxNumReorderSamples != 0. - MediaCodecRenderer relabels output buffers of such formats with the queued timestamps, in output order. - MediaCodecVideoRenderer uses the very-late threshold in shouldDropOutputBuffer and disables shouldDropBuffersToKeyframe for them.
Addresses the remaining points from the 2026-08-05 review on androidx#3352. - BoxParser: only set hasReliablePresentationTimestamps=false for sample tables that have samples. FragmentedMp4Extractor also parses the moov box's empty sample table, which never has a ctts box because composition offsets come from trun boxes, so every fragmented video track with B-frames was being flagged and its correct timestamps scrambled. - MediaCodecRenderer: keep the relabeling queue in step when the codec discards samples without output. BUFFER_FLAG_DECODE_ONLY samples are no longer recorded, and each output's echoed timestamp is matched against the recorded ones. Pending samples queued more than maxNumReorderSamples before the match can't still be awaiting output (for example RASL pictures discarded after seeking to a CRA picture), so they're dropped. Outputs that don't match keep the codec's timestamp, which also makes the output side independent of codecInputFormat vs inputFormat during format changes. - Tests: extractor tests for no ctts with and without an edit list, with FLAG_OMIT_TRACK_SAMPLE_TABLE, non-reordered video, ctts present, and fragmented MP4 (three small generated H.264 assets); renderer tests for reordered output, codec-discarded samples, VFR with reordering, a seamless format change, EOS and flush. - Comments tightened; removed references to review point numbers.
With the relabeling in MediaCodecRenderer, output buffers of a track with unreliable timestamps carry their decode-time slots in display order, which for constant frame rate content are their exact presentation times. earlyUs is therefore a real lateness signal again, so the widened threshold in shouldDropOutputBuffer and the disabled shouldDropBuffersToKeyframe aren't needed, and keeping them would weaken recovery from genuine decoder slowness (point 3 of the 2026-08-05 review on androidx#3352). Verified on a Google TV Streamer 4K (c2.mtk.hevc.decoder) with the stock drop policy: a 95s B-frame HEVC file without ctts plays with 0 dropped frames and monotonic relabeled timestamps.
410db95 to
8abccee
Compare
…hout ctts Squash of androidx#3352 (325f2c5..410db95, previously based on release) applied to main. Conflicts were resolved mechanically: main has since removed Format#hasPrerollSamples, and its MediaCodecRenderer additions sit next to the ones in this change. MP4 video tracks that reorder frames but have no ctts box give every sample a presentation time equal to its decode time, for example a raw H.265 elementary stream muxed with ffmpeg -fflags +genpts -c:v copy. MediaCodec echoes those timestamps back on frames that come out in display order, so the renderer sees non-monotonic timestamps and drops ~30% of frames as late. See androidx#3347. - Format#hasReliablePresentationTimestamps, set false by BoxParser for a video track with no ctts and maxNumReorderSamples != 0. - MediaCodecRenderer relabels output buffers of such formats with the queued timestamps, in output order. - MediaCodecVideoRenderer uses the very-late threshold in shouldDropOutputBuffer and disables shouldDropBuffersToKeyframe for them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Addresses the remaining points from the 2026-08-05 review on androidx#3352. - BoxParser: only set hasReliablePresentationTimestamps=false for sample tables that have samples. FragmentedMp4Extractor also parses the moov box's empty sample table, which never has a ctts box because composition offsets come from trun boxes, so every fragmented video track with B-frames was being flagged and its correct timestamps scrambled. - MediaCodecRenderer: keep the relabeling queue in step when the codec discards samples without output. BUFFER_FLAG_DECODE_ONLY samples are no longer recorded, and each output's echoed timestamp is matched against the recorded ones. Pending samples queued more than maxNumReorderSamples before the match can't still be awaiting output (for example RASL pictures discarded after seeking to a CRA picture), so they're dropped. Outputs that don't match keep the codec's timestamp, which also makes the output side independent of codecInputFormat vs inputFormat during format changes. - Tests: extractor tests for no ctts with and without an edit list, with FLAG_OMIT_TRACK_SAMPLE_TABLE, non-reordered video, ctts present, and fragmented MP4 (three small generated H.264 assets); renderer tests for reordered output, codec-discarded samples, VFR with reordering, a seamless format change, EOS and flush. - Comments tightened; removed references to review point numbers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the relabeling in MediaCodecRenderer, output buffers of a track with unreliable timestamps carry their decode-time slots in display order, which for constant frame rate content are their exact presentation times. earlyUs is therefore a real lateness signal again, so the widened threshold in shouldDropOutputBuffer and the disabled shouldDropBuffersToKeyframe aren't needed, and keeping them would weaken recovery from genuine decoder slowness (point 3 of the 2026-08-05 review on androidx#3352). Verified on a Google TV Streamer 4K (c2.mtk.hevc.decoder) with the stock drop policy: a 95s B-frame HEVC file without ctts plays with 0 dropped frames and monotonic relabeled timestamps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ctts
|
Thanks for the detailed review, and sorry for the long gap. I've reworked this against all six 1. Flag not set on early-return paths. Fixed, though the placement in my first attempt 2. Missing 3. Don't disable recovery from genuine slowness. Agreed, and I removed the drop-policy change 4. 5. Tunneling. Nothing is recorded in tunneling mode, since output buffers are never dequeued 6. VFR + reordering can't be reconstructed. Agreed, and the old test didn't exercise it. With Input/output count mismatch (from your test list, and the most significant thing this round
Tests (your list → where):
On-device (Google TV Streamer 4K,
In the seek run, the prune fired once, on the first frame after seeking to a CRA, when the Unit tests: extractor |
8abccee to
2a6eaa9
Compare
MP4 video tracks whose frames are reordered (B-frames) but that have no
cttsbox give everysample a presentation time equal to its decode time. Files like this are produced by muxing a
raw H.264/H.265 elementary stream with
ffmpeg -fflags +genpts -c:v copy, which is a commonpipeline for x265 CLI output, since the CLI has no muxer. MediaCodec echoes these decode-order
timestamps back on frames that come out in display order, so the renderer sees non-monotonic
timestamps and
MediaCodecVideoRendererdrops ~30% of frames as late. See #3347 for theinvestigation; VLC avoids the problem by treating missing
cttsas an unknown PTS.Changes:
Format#hasReliablePresentationTimestamps(unstable API, defaulttrue).BoxParsersets it tofalsefor a video track with samples, noctts, andmaxNumReorderSamples != 0. Fragmented MP4 is unaffected.MediaCodecRenderer: for such formats, records each queued sample's timestamp and relabelseach output buffer with the queued timestamps in output order. It resyncs when the codec
discards samples without output (decode-only buffers, RASL pictures after a seek) and leaves
outputs it can't match untouched. Not used in tunneling mode.
MediaCodecVideoRenderer's frame-drop policy is unchanged.Behavior for every other track is unchanged.
Tested with new extractor and renderer unit tests, and on a Google TV Streamer 4K (
c2.mtk.hevc.decoder): the affected file now plays with 0 dropped frames under the stock drop policy, including across seeks. Details in the review reply below.